React Fiber

在 React 16 之前,VirtualDOM 的更新采用的是 Stack 架构实现的,也就是循环递归方式。不过,这种对比方式有明显的缺陷,就是一旦任务开始进行就无法中断,如果遇到应用中组件数量比较庞大,那么 VirtualDOM 的层级就会比较深,带来的结果就是主线程被长期占用,进而阻塞渲染、造成卡顿现象。

为了避免出现卡顿等问题,我们必须保障在执行更新操作时计算时不能超过 16ms,如果超过 16ms,就需要先暂停,让给浏览器进行渲染,后续再继续执行更新计算。Fiber 是 React v16 中新的 reconciliation 引擎,是核心 diff 算法的重新实现方式。就是为了支持“可中断渲染”而创建的。

React Fiber 的目标是提高对动画,布局,手势,暂停,中止或者重用任务的能力及为不同类型的更新分配优先级,及新的并发原语等领域的适用性。它的主要特性是 incremental rendering(增量渲染): 将渲染任务拆分为小的任务块并将任务分配到多个帧上的能力。

Fiber 架构

Fiber 节点拥有 return, child, sibling 三个属性,分别对应父节点,第一个孩子,它右边的兄弟,有了它们就足够将一棵树变成一个链表,实现深度优化遍历。链表在执行遍历操作时是支持断点重启的。

Fiber 的关键特性如下:

  1. 增量渲染(把渲染任务拆分成块,匀到多帧)
  2. 更新时能够暂停,终止,复用渲染任务
  3. 给不同类型的更新赋予优先级
  4. 并发方面新的基础能力

增量渲染用来解决掉帧的问题,渲染任务拆分之后,每次只做一小段,做完一段就把时间控制权交还给主线程,而不像之前长时间占用。这种策略叫做 cooperative scheduling(合作式调度),操作系统的 3 种任务调度策略之一(Firefox 还对真实 DOM 应用了这项技术)。

Fiber 任务分为 5 个具体目标:

  1. 把可中断的工作拆分成小任务
  2. 对正在做的工作调整优先次序、重做、复用上次(做了一半的)成果
  3. 在父子任务之间从容切换(yield back and forth),以支持 React 执行过程中的布局刷新
  4. 支持 render()返回多个元素
  5. 更好地支持 error boundary

Fiber 对生命周期的影响

Fiber 机制一定程度上的影响了部分生命周期的调用,并且也引入了新的 2 个 API 来解决问题。

Fiber 本质上是一个虚拟的堆栈帧,调度器会按照优先级自由调度这些帧,将之前的同步渲染改成了异步渲染,在不影响体验的情况下去分段计算更新。

对于如何区别优先级,React 有自己的一套逻辑:

  1. 对于动画这种实时性很高的东西,也就是 16 ms 必须渲染一次保证不卡顿的情况下,React 会每 16 ms(以内) 暂停一下更新,返回来继续渲染动画。

对于异步渲染,现在渲染有两个阶段:reconciliationcommit 。前者过程是可以打断的,后者不能暂停,会一直更新界面直到完成。

Reconciliation 阶段

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

Commit 阶段

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

因为 reconciliation 阶段是可以被打断的,所以 reconciliation 阶段会执行的生命周期函数就可能会出现调用多次的情况,从而引起 Bug。所以对于 reconciliation 阶段调用的几个函数,除了 shouldComponentUpdate 以外,其他都应该避免去使用,并且 V16 中也引入了新的 API 来解决这个问题。

getDerivedStateFromProps 用于替换 componentWillReceiveProps ,该函数会在初始化和 update 时被调用

class ExampleComponent extends React.Component {
  // Initialize state in constructor,
  // Or with a property initializer.
  state = {};

  static getDerivedStateFromProps(nextProps, prevState) {
    if (prevState.someMirroredValue !== nextProps.someValue) {
      return {
        derivedData: computeDerivedState(nextProps),
        someMirroredValue: nextProps.someValue,
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

getSnapshotBeforeUpdate 用于替换 componentWillUpdate ,该函数会在 update 后 DOM 更新前被调用,用于读取最新的 DOM 数据。

双缓存 Fiber 树

什么是“双缓存”

当我们用 canvas 绘制动画,每一帧绘制前都会调用 ctx.clearRect 清除上一帧的画面。

如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。

为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。

这种在内存中构建并直接替换的技术叫做双缓存 (opens new window)。

React 使用“双缓存”来完成 Fiber 树的构建与替换——对应着 DOM 树的创建与更新。

alternate 备用

在任何情况下,每个组件实例最多有两个 fiber 何其关联。一个是被 commit 过后的 fiber,即它所包含的副作用已经被应用到了 DOM 上了,称它为 current fiber, 另一个是现在未被 commit 的 fiber,称为 work-in-progress fiber

current fiberalternatework-in-progress fiber, 而 work-in-progress fiberalternatecurrent fiber

即当 workInProgress Fiber 树构建完成交给 Renderer 渲染在页面上后,应用根节点的 current 指针指向 workInProgress Fiber 树,此时 workInProgress Fiber 树就变为 current Fiber 树。

每次状态更新都会产生新的 workInProgress Fiber 树,通过 current 与 workInProgress 的替换,完成 DOM 更新。

其他

  • 你是如何理解 fiber 的?
  • 你对 Time Slice 的理解?

Fiber 是对核心算法的一次重新实现。因为 javascript 是单线程的。react 的组件设计,如果你的一个组件加载或者更新时,带动 200 个组件更新,那么它会等这 200 个组件更新完再让出进程,如果这个时候用户有交互,是没有反应的(如果说 200 个组件需要 200 毫秒,这 200 毫秒内交互无效),为了提高用户体验,引入了 Fiber。

调度的最小单位——Fiber。

异步渲染中的 Fiber 的做法是:分片。把一个很耗时的任务分成很多小片,Fiber 之前的架构是同步更新,遍历,从根组件开始到子节点。

假如更新一个组件需要 1 毫秒,如果有 200 个组件要更新,那就需要 200 毫秒,在这 200 毫秒的更新过程中,浏览器那个唯一的主线程都在专心运行更新操作,无暇去做任何其他的事情。想象一下,在这 200 毫秒内,用户往一个 input 元素中输入点什么,敲击键盘也不会获得响应,因为渲染输入按键结果也是浏览器主线程的工作,但是浏览器主线程被 React 占着呢,抽不出空。最后的结果就是用户敲了按键看不到反应,等 React 更新过程结束之后,咔咔咔那些按键一下子出现在 input 元素里了。

这就是所谓的界面卡顿,很不好的用户体验。

React Fiber 更新过程

React Fiber 更新过程被分为两个阶段(Phase):第一个阶段 Reconciliation Phase 和第二阶段 Commit Phase。第一阶段,Fiber 会找到需要更新哪些 DOM,这个阶段可以被打算;但到了第二阶段,就会一鼓作气把 DOM 更新完,绝不会被打断。

Last Updated:
Contributors: yiliang114